/*
call、apply和bind的原理
*/

/*
call和apply
call 和 apply 是 Function 构造函数原型对象上的方法，所有的函数都可以调用 call 和 apply
，作用是可以改变调用 call 和 apply 函数内部的 this 指向，并执行函数。
*/

//call 的使用方法

/*function fn() {
  console.log('this',this);
  console.log(' arguments', arguments);
  //不指定替换的 this，则调用 call 的函数在运行时决定 this 指向，当前案例中在浏览器中运行，则指向 window 对象。
}

fn.call();*/


// Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
// Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]


/*function fn() {
  console.log(this, arguments);
}

fn.call('hello', '1', '2');*/

// String { "hello" }
// Arguments(2) ["1", "2", callee: ƒ, Symbol(Symbol.iterator): ƒ]

/*
由于 call 是函数原型的方法，当然也可以被 call 自己调用
，在下面的案例中，第一个 call 内部的 this 为调用者 fn1
，通过第二个 call 将第一个 call 内部的 this 由 fn1 更改为 fn2
，1 和 2 作为参数传递给第一个 call
，而 1 又作为第一个 call 内部 this 指向的 fn2 内部的 this
，2 作为 fn2 的参数
，最后执行 fn2，固有上面执行结果。
*/
/*function fn1() {
  console.log(this, arguments);
}

function fn2() {
  console.log('this:', this);
  console.log('arguments:', arguments);
}

fn1.call.call(fn2, '1', '2'); */

// String { "1" }
// Arguments ["2", callee: ƒ, Symbol(Symbol.iterator): ƒ]


// 不管.多少次call  fn.call.call.call.call = 原型上的call方法
// fn.call.call.call.call 就等价于 fn.call.call
// call.call(fn1)
/*function fn(a,b){
  console.log(this,a,b);
}
function fn1() {
  console.log('100')
}
fn.call.call.call.call.call(fn1); // 100*/


//----------
//call的实现
// context 参数为要替换的 this
Function.prototype.call = function (context) {
  // 将传入的 this 转换成对象，若没传则使用 window 作为 this
  context = context ? Object(context) : window;
  /*
    let x = Object(123);
    console.log(typeof x); //object
    console.log(x); //[Number: 123]
    console.log(x.a); //undefined
  */

  // 将调用 call 的函数作为属性赋值给传入的context的fn
  context.fn = this/*←这个this就是调用call的那个函数*/;

  var args = [];

  // 将传递给调用 call 函数的参数转化成字符串取值的形式
  for (var i = 1; i < arguments.length; i++) {
    // args ['arguments[1]', 'arguments[2]']
    args.push('arguments[' + i + ']');
  }

  // 利用 eval 执行 context.fn，并利用数组转换字符串的 toString 去掉 [ ]
  var result = eval('context.fn(' + args + ')');

  // 删除 context 上多余的 fn 属性
  delete context.fn;

  return result;
};






//----
//apply 的实现原理
Function.prototype.apply = function(context,args){
  context = context ? Object(context) : global;
  context.fn = this; // call的特点就是将自己 放到一个对象上 通过对象调用
  let r = context.fn(...args);
  delete context.fn;
  return r;
}





//----
/*
bind 函数是 Function 原型对象上的方法
，bind 的作用是可以将调用它的函数内部的 this 绑定成所指定的 this
，第一个参数为指定的 this
，与 call 和 apply 不同的是
，调用 bind 的函数并不会执行
，而是返回一个新的函数
，新的函数调用时传入的参数会和 bind 调用时传入的除第一个以外的参数进行合并
，并作为调用 bind 的函数执行的参数，下面是 bind 的基本用法。
*/

/*var obj = {
  name: 'Shen'
};

function animal(name, age) {
  console.log(this.name + ' have a ' + name + ' is ' + age + ' years old.');
}

var bindFn = animal.bind(obj, 'cat');
bindFn(2);
// Shen have a cat is 2 years old.*/


//bind实现
// context 参数为要绑定的 this
Function.prototype.bind = function (context) {
  // this 为调用 bind 的函数
  var self = this;

  // 收集除了 context 以外所有的参数
  var bindArgs = Array.prototype.slice.call(arguments, 1);

  // 返回的新函数 fBound
  function fBound() {
    // 收集 fBound 的参数
    var args = Array.prototype.slice.call(arguments);

    // 执行调用 bind 的函数
    // 若是普通函数调用，this 为 context，若是作为构造函数则 this 为实例
    self.apply(this instanceof fBound ? this : context, bindArgs.concat(args));
  }

  // 用来继承的中间函数
  function fNOP() {}

  // 作为构造函数调用 fBound 时，为了实例可以找到调用 bind 函数的原型对象，进行继承
  if (this.prototype) {
    // Function.prototype 为函数，可以调用 bind，当时没有原型对象，所以要判断
    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
  }

  //↑↓等价
  // fBound.prototype = Object.create(this.prototype);

  return fBound;
}

/*
在上面代码中，如果调用 bind 返回函数作为构造函数使用，则需要通过继承找回原函数的属性和方法
，但是有一个特例，就是 Function.prototype，类型为函数，却没有 prototype 属性，所以需要判断。
*/
